﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;

namespace MenuLib
{
    [Serializable]
    public class Menu : ISerializable
    {
        public Menu()
        {
            Text = "Menu";

            items = new MenuItemCollection(this);

            currentItemChar = new CurrentItemChar(this);
            currentItemChar.Reset();

            itemsByHotKey = new Dictionary<char, int>();
            itemsBySelection = new Dictionary<char, int>();
        }

        public string Text
        {
            get { return text; }
            set { text = value; }
        }

        public MenuItemCollection Items
        {
            get { return items; }
        }

        internal void SetHotKey(char hotKey, MenuItem menuItem)
        {
            if (!HotKeyAvailable(hotKey))
            {
                throw new ApplicationException(string.Format("{0}: hot key in use", hotKey));
            }

            int index = Items.IndexOf(menuItem);

            if (0 > index)
            {
                throw new ApplicationException("Menu item not exists");
            }

            ItemsByHotKey.Add(hotKey, index);
        }

        public bool HotKeyAvailable(char hotKey)
        {
            hotKey = char.ToLower(hotKey);
            return
                !(
                    currentItemChar.Exhausted ||
                    ItemsByHotKey.ContainsKey(hotKey) ||
                    ItemsBySelection.ContainsKey(hotKey)
                );
        }

        public void Display()
        {
            DisplayHeader();
            DisplayItems();
        }

        public void Save(string path)
        {
            FileStream fs = new FileStream(path, FileMode.Create);
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(fs, this);
            fs.Close();
        }

        public static Menu Load(string path)
        {
            FileStream fs = new FileStream(path, FileMode.Open);
            BinaryFormatter formatter = new BinaryFormatter();
            return (Menu)formatter.Deserialize(fs);
        }

        public void Run()
        {
            bool quit = false;

            while (!quit)
            {
                Display();

                while (true)
                {
                    Console.Write("Please type selection/hotkey/command + <Enter>('{0}' for help): ", helpCmd);

                    string userInput = Console.ReadLine();

                    switch (ProcessUserInput(userInput))
                    {
                        case UserInput.CmdDisplay:
                        case UserInput.CmdHelp:
                            continue;

                        case UserInput.Invalid:
                            DisplayHelp();
                            continue;

                        case UserInput.CmdQuit:
                            quit = true;
                            break;

                        case UserInput.HotKey:
                        case UserInput.ItemSelection:
                            break;

                        default:
                            break;
                    }

                    break;
                }
            }
        }

        protected internal void ItemRemoved(int itemIndex)
        {
            ItemsByHotKey.RemoveFirstByValue((int i) => i == itemIndex);

            if (!ItemsBySelection.RemoveFirstByValue((int i) => i == itemIndex))
            {
                throw new ApplicationException("Item doesn't exist");
            }

            if (itemIndex != Items.Count)
            {
                RearrangeSelections();
            }
        }

        protected internal void ItemAdded(int itemIndex)
        {
            MenuItem addedItem = Items[itemIndex];

            addedItem.OwnerMenu = this;

            if (itemIndex == Items.Count - 1)
            {
                AssignSelectionToItem(itemIndex);
            }
            else
            {
                RearrangeSelections();
            }
        }

        private Dictionary<char, int> ItemsBySelection
        {
            get { return itemsBySelection; }
        }

        private Dictionary<char, int> ItemsByHotKey
        {
            get { return itemsByHotKey; }
        }

        private void RearrangeSelections()
        {
            currentItemChar.Reset();
            ItemsBySelection.Clear();

            for (int i = 0; i < Items.Count; i++)
            {
                AssignSelectionToItem(i);
            }

            ItemsByHotKey.Clear();

            for (int i = 0; i < Items.Count; i++)
            {
                Items[i].OwnerMenu = this;
            }
            
        }

        private void AssignSelectionToItem(int itemIndex)
        {
            if (!currentItemChar.MoveNext())
            {
                throw new ApplicationException("Menu full");
            }

            ItemsBySelection.Add(currentItemChar.Current, itemIndex);

            Items[itemIndex].SelectionText = string.Format("{0})", currentItemChar.Current);
        }

        private void DisplayHelp()
        {
            Console.WriteLine(
                "? Type [item selection char/hot key char]+<Enter> to make a selection");
            Console.WriteLine("? Type '{0}'+<Enter> to print this message again", helpCmd);
            Console.WriteLine("? Type '{0}'+<Enter> to display the menu", displayCmd);
            Console.WriteLine("? Type '{0}'+<Enter> to quit", quitCmd);
        }

        private void DisplayHeader()
        {
            Console.WriteLine("**********************************************************");
            Console.WriteLine("*** {0}", Text);
            Console.WriteLine("**********************************************************");
        }

        private void DisplayItems()
        {
            Items.ForEach((MenuItem item) => item.Display());
        }

        private enum UserInput
        {
            ItemSelection,
            HotKey,
            CmdHelp,
            CmdDisplay,
            CmdQuit,
            Invalid,
        }

        private UserInput ProcessUserInput(string userInput)
        {
            if (userInput.Length == 1)
            {
                int selectedItemIndex;
                char userInputChar = char.ToLower(userInput[0]);

                if (ItemsByHotKey.TryGetValue(userInputChar, out selectedItemIndex))
                {
                    Items[selectedItemIndex].OnSelect(EventArgs.Empty);
                    return UserInput.HotKey;
                }
                else if (ItemsBySelection.TryGetValue(userInputChar, out selectedItemIndex))
                {
                    Items[selectedItemIndex].OnSelect(EventArgs.Empty);
                    return UserInput.ItemSelection;
                }
            }

            if (string.Compare(helpCmd, userInput, true) == 0)
            {
                DisplayHelp();
                return UserInput.CmdHelp;
            }
            else if (string.Compare(displayCmd, userInput, true) == 0)
            {
                Display();
                return UserInput.CmdDisplay;
            }
            else if (string.Compare(quitCmd, userInput, true) == 0)
            {
                return UserInput.CmdQuit;
            }

            Console.WriteLine("'{0}': Invalid input", userInput);
            return UserInput.Invalid;
        }

        #region Data members

        private string text;
        private MenuItemCollection items;
        private CurrentItemChar currentItemChar;
        private Dictionary<char, int> itemsBySelection;
        private Dictionary<char, int> itemsByHotKey;

        private readonly string helpCmd = "help";
        private readonly string quitCmd = "quit";
        private readonly string displayCmd = "display";

        #endregion

        [Serializable]
        class CurrentItemChar : IEnumerator<char>
        {
            public CurrentItemChar(Menu menu)
            {
                this.menu = menu;
                Reset();
            }

            public bool Exhausted
            {
                get
                {
                    return current == 'z';
                }
            }

            #region IEnumerator<char> Members

            public char Current
            {
                get { return current; }
            }

            #endregion

            #region IDisposable Members

            public void Dispose()
            {
            }

            #endregion

            #region IEnumerator Members

            object System.Collections.IEnumerator.Current
            {
                get { return (this as IEnumerator<char>).Current; }
            }

            public bool MoveNext()
            {
                while (!Exhausted)
                {
                    if (!menu.ItemsByHotKey.ContainsKey(++current))
                    {
                        return true;
                    }
                }

                return false;
            }

            public void Reset()
            {
                current = 'a';
                --current;
            }

            #endregion

            private char current;
            private Menu menu;
        }

        #region ISerializable Members

        const string textValueName = "Text";
        const string itemsValueName = "Items";
        const string currentItemCharValueName = "CurrentItemChar";
        const string itemsBySelectionValueName = "ItemsBySelection";
        const string itemsByHotKeyValueName = "ItemsByHotKey";

        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue(textValueName, Text);
            info.AddValue(itemsValueName, Items);
            info.AddValue(currentItemCharValueName, currentItemChar);
            info.AddValue(itemsBySelectionValueName, ItemsBySelection);
            info.AddValue(itemsByHotKeyValueName, ItemsByHotKey);
        }

        protected Menu(SerializationInfo info, StreamingContext context)
        {
            Text = info.GetString(textValueName);
            items = (MenuItemCollection)info.GetValue(
                itemsValueName, typeof(MenuItemCollection));

            currentItemChar = (CurrentItemChar)info.GetValue(
                currentItemCharValueName, typeof(CurrentItemChar));

            itemsBySelection = (Dictionary<char, int>)info.GetValue(
                itemsBySelectionValueName, typeof(Dictionary<char, int>));

            itemsByHotKey = (Dictionary<char, int>)info.GetValue(
                itemsByHotKeyValueName, typeof(Dictionary<char, int>));
        }

        #endregion
    }

    
}
